/******************************************************************************/
package jamie.org.sil.www.usxUtils;

import java.io.*;


/******************************************************************************/
/**
 * Handle Unicode BOM marks.
 * <p>
 *
 * The Unicode BOM mark optionally appears at the start of Unicode
 * files, and indicates the byte-order in multi-byte characters.
 * It seems to be a somewhat contentious beastie, and in particular
 * there is evidence on the web that Java file IO does not handle it
 * correctly -- instead of consuming it internally and using it to
 * control the input process, it actually returns it to the caller
 * (which almost invariably ends up very confused as a result).
 * <p>
 *
 * The present class addresses this -- it strips any BOM out of the
 * file, and then parses it and uses it to control the input processing.
 * <p>
 *
 * The nature of the BOM mark varies according to the encoding being
 * used :-
 * <p><pre>
 *   00 00 FE FF = UTF-32, big-endian<br/>
 *   FF FE 00 00 = UTF-32, little-endian<br/>
 *   FE FF       = UTF-16, big-endian<br/>
 *   FF FE       = UTF-16, little-endian<br/>
 *   EF BB BF    = UTF-8<br/>
 * </pre><p>
 * The code is a modified version of something I found on the web.
 *
 * @author ARA "Jamie" Jamieson
 */

public class UnicodeReader extends Reader
{
  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                                Public                                  **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /**
   * Gets the encoding used by default in the absence of any indication in the
   * input stream as to what encoding should be used.
   *
   * @return Encoding.
   */

  public String getDefaultEncoding () { return m_DefaultEncoding; }


  /****************************************************************************/
  /**
   * Gets the encoding specified for this particular stream.
   *
   * @return Encoding.
   */

  public String getEncoding () { return null == m_StreamReaderIn ? "" : m_StreamReaderIn.getEncoding(); }


  /****************************************************************************/
  /**
   * Creates a UnicodeReader associated with a given input stream, using
   * UTF-8 as the default encoding (ie where no encoding is indicated in the
   * input stream itself).
   *
   * @param in Input stream.
   */

  public UnicodeReader (InputStream in)
  {
    m_PushbackIn = new PushbackInputStream(in, C_BomSize);
    m_DefaultEncoding = "UTF-8";
  }


  /****************************************************************************/
  /**
   * Creates a UnicodeReader associated with a given input stream, using
   * a given default encoding (ie where no encoding is indicated in the
   * input stream itself).
   *
   * @param in               Input stream.
   * @param defaultEncoding  Default encoding (eg "UTF-8").
   */

  public UnicodeReader (InputStream in, String defaultEncoding)
  {
    m_PushbackIn = new PushbackInputStream(in, C_BomSize);
    m_DefaultEncoding = defaultEncoding;
  }


  /****************************************************************************/
  /**
   * Close the input stream.
   *
   * @throws IOException
   */

  public void close() throws IOException
  {
    init();
    m_StreamReaderIn.close();
  }


  /****************************************************************************/
  /**
   * Read from the input stream.
   *
   * @param buf          Buffer into which to read.
   * @param offset       Offset into buffer at which to start placing data.
   * @param len          Number of characters to read.
   * @return             Number of characters actually read.
   * @throws IOException As per the equivalent InputStreamReader operation.
   */

  public int read(char[] buf, int offset, int len) throws IOException
  {
    init();
    return m_StreamReaderIn.read(buf, offset, len);
  }





  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                              Protected                                 **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /**
   * Initialises the stream.  We read four bytes, which is the maximum possible
   * length for the BOM, and then push back anything which can't form part of a
   * BOM.
   *
   * @throws IOException
   */

  protected void init() throws IOException
  {
    if (m_StreamReaderIn != null) return;

    byte bom[] = new byte[C_BomSize];
    String encoding = null;
    int n;
    int unread;

    for (int i = 0; i < C_BomSize; ++i) bom[i] = 0;

    n = m_PushbackIn.read(bom, 0, bom.length);

    if ( (bom[0] == (byte) 0xEF) && 
         (bom[1] == (byte) 0xBB) &&
         (bom[2] == (byte) 0xBF) )
    {
      encoding = "UTF-8";
      unread = n - 3;
    }
    else if ( (bom[0] == (byte) 0xFE) && 
              (bom[1] == (byte) 0xFF) )
    {
      encoding = "UTF-16BE";
      unread = n - 2;
    }
    else if ( (bom[0] == (byte) 0xFF) &&
              (bom[1] == (byte) 0xFE) )
    {
      encoding = "UTF-16LE";
      unread = n - 2;
    }
    else if ( (bom[0] == (byte) 0x00) &&
              (bom[1] == (byte) 0x00) &&
              (bom[2] == (byte) 0xFE) &&
              (bom[3] == (byte) 0xFF))
   {
     encoding = "UTF-32BE";
     unread = n - 4;
   }
   else if ( (bom[0] == (byte) 0xFF) &&
             (bom[1] == (byte) 0xFE) &&
             (bom[2] == (byte) 0x00) &&
             (bom[3] == (byte) 0x00))
   {
     encoding = "UTF-32LE";
     unread = n - 4;
   }
   else
   {
     // No BOM mark.
     encoding = m_DefaultEncoding;
     unread = n;
   }

   if (unread > 0)
     m_PushbackIn.unread(bom, (n - unread), unread);
   else if (unread < -1)
     m_PushbackIn.unread(bom, 0, 0);

   if (encoding == null)
     m_StreamReaderIn = new InputStreamReader(m_PushbackIn);
   else
     m_StreamReaderIn = new InputStreamReader(m_PushbackIn, encoding);
  }





  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                               Private                                  **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  private static final int C_BomSize = 4; // Maximum number of bytes in a marker.

  private PushbackInputStream m_PushbackIn = null;
  private InputStreamReader m_StreamReaderIn = null;
  private String m_DefaultEncoding = "";
}
